gRPC 的优势
gRPC 是微服务通信中推荐重点掌握的方案,特别适合以下场景:
- 跨语言通信 -- gRPC 支持 Java、Python、Go、Node.js 等多种语言,是连接不同技术栈服务的理想纽带
- 高性能 -- 基于 HTTP/2 和 Protocol Buffers,序列化/反序列化性能远超 JSON
- 强类型约束 -- 通过
.proto文件定义服务接口,自动生成类型安全的客户端和服务端代码 - 双向流 -- 支持客户端流、服务端流和双向流通信
在 NestJS 中,gRPC 传输器可以动态绑定 .proto 文件到客户端和服务端,使远程调用变得简单直观。
安装依赖
pnpm add @grpc/grpc-js @grpc/proto-loader
bash
.proto 文件详解
.proto 文件是 gRPC 的核心,它使用 Protocol Buffers 语法定义服务接口和消息类型。
基本示例
// hero.proto
syntax = "proto3";
// package 类似于命名空间,用于区分不同的 proto 文件
package hero;
// 定义服务
service HeroesService {
rpc FindOne (HeroById) returns (Hero) {}
rpc FindMany (HeroByIdList) returns (HeroList) {}
}
// 请求消息
message HeroById {
int32 id = 1;
}
message HeroByIdList {
repeated int32 ids = 1;
}
// 响应消息
message Hero {
int32 id = 1;
string name = 2;
}
message HeroList {
repeated Hero heroes = 1;
}
protobuf
proto3 语法要点
| 语法 | 说明 |
|---|---|
syntax = "proto3" | 指定使用 proto3 语法(不声明则按 proto2 编译) |
package | 包名,类似于命名空间,不同语言对应不同的代码结构(C++ 对应 namespace,Go 对应 package) |
service | 定义 RPC 服务接口,protobuf 编译器会根据选择的语言自动生成服务接口代码和存根 |
rpc | 定义远程方法,指定请求和响应类型 |
message | 定义消息结构体,一个 .proto 文件中可以有多个 message 定义 |
int32, string | 字段类型 |
= 1, = 2 | 字段编号,用于二进制序列化,一旦定义后不可更改 |
repeated | 表示数组/列表 |
字段编号规则
消息中定义的字段每个都要有一个唯一的编号,在编码时用来确定字段,一旦类型定义之后不可更改:
- 1 ~ 15 之间的编号只需 1 个字节编码,应为经常使用的字段预留
- 16 ~ 2047 需要 2 个字节
- 最小值 1,最大值 2^29 - 1(536,870,911)
- 19000 ~ 19999 预留给 Protocol Buffers 实现,不可使用
字段规则
- 单数(singular):单个值,默认行为
- repeated:对应数组
proto3 中,标量数字类型的 repeated 字段默认使用 packed 编码。
注释
.proto 文件使用 C/C++ 风格的注释:// 和 /* ... */。
保留字段(reserved)
删除或注释掉的字段编号不可被复用,否则会导致数据损坏和隐藏 bug。使用 reserved 显式标记已删除的字段编号,编译器会在有人误用时发出提示:
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
protobuf
可以使用 max 关键字表示最大编号值,如 40 to max。注意 reserved 中不可混用字段编号和字段名。
标量类型一览
| .proto 类型 | 说明 | Go 类型 |
|---|---|---|
| double | float64 | |
| float | float32 | |
| int32 | 可变长度编码,负数编码效率低,负值场景请用 sint32 | int32 |
| int64 | 可变长度编码,负数编码效率低,负值场景请用 sint64 | int64 |
| uint32 | 可变长度编码 | uint32 |
| uint64 | 可变长度编码 | uint64 |
| sint32 | 可变长度编码,有符号整型 | int32 |
| sint64 | 可变长度编码,有符号整型 | int64 |
| fixed32 | 总是 4 字节,值大于 2^28 时比 uint32 更高效 | uint32 |
| fixed64 | 总是 8 字节,值大于 2^56 时比 uint64 更高效 | uint64 |
| sfixed32 | 总是 4 字节 | int32 |
| sfixed64 | 总是 8 字节 | int64 |
| bool | bool | |
| string | UTF-8 编码或 7-bit ASCII,不超过 2^32 | string |
| bytes | 任意长度不超过 2^32 的字节序列 | byte |
默认值
消息解析时,未包含的字段使用默认值:
- 字符串:空字符串
- 字节:空字节
- 布尔值:false
- 数值类型:0
- 枚举:第一个定义的枚举项(必须为 0)
- message 字段:取决于编程语言
- repeated 字段:空列表
对于标量类型,消息解析后无法判断该值是默认值还是未被设置的值,设计时需明确这一点。
枚举
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
protobuf
枚举规则:
- 第一个元素必须设置为 0(保证默认值存在,同时兼容 proto2)
- 枚举值必须在 32-bit 整型范围内,不建议使用负数
- 通过
allow_alias = true可以为不同常量分配相同值 - 枚举可以定义在消息内部或外部
复合类型
使用其他消息类型
可以将已定义的消息类型作为另一个消息的字段类型:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
protobuf
导入定义
消息定义在不同 .proto 文件中时,使用 import 导入:
import "myproject/other_protos.proto";
protobuf
默认只能使用直接导入的定义。若 B import A,C import B,C 并不能自动使用 A 的定义。若需要传递导入,在 B 中使用 import public:
import public "a.proto"
protobuf
内嵌类型
可以在消息类型中定义和使用嵌套消息类型:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
protobuf
在父消息外部引用时使用 Parent.Type 格式:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
protobuf
Any 类型
Any 消息类型可以作为嵌入类型使用而无须预定义,包含任意序列化消息及其类型的全局唯一标识符 URL:
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
protobuf
默认 URL 格式:type.googleapis.com/packagename.messagename。
Oneof
当一个消息有很多字段但同一时间只会设置其中一个时,使用 oneof 节省内存:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
protobuf
Oneof 特性:
- 设置一个字段会自动清空其他字段,多次设置只有最后一次生效
- 不可以使用
repeated - 解析时如果遇到同 oneof 的多个成员,只有最后一个被解析
Maps
protobuf 提供了便捷的映射语法:
map<key_type, value_type> map_field = N;
protobuf
key_type可以是任何整型或字符串类型(浮点型和 bytes 除外,枚举也不可)value_type可以是除 map 以外的任何类型- Map 字段不可以是
repeated - Map 的元素顺序不确定,编码时重复的 key 使用最后一个值
更新消息类型的规则
在不破坏现有消息类型的前提下更新,需遵守以下规则:
- 不要更改现有字段的字段编号
- 新增字段时,旧消息格式仍可被新代码解析(新字段使用默认值),新消息也可被旧代码解析(忽略新字段)
- 删除字段时,使用
reserved或添加OBSOLETE_前缀,确保编号不被复用 int32、uint32、int64、uint64和bool之间互相兼容sint32和sint64相互兼容,但与其他整型不兼容string和bytes在有效 UTF-8 时互相兼容bytes与内嵌消息(如果字节包含消息的编码版本)兼容fixed32与sfixed32兼容,fixed64与sfixed64兼容enum与int32、uint32、int64、uint64兼容(值不同时自动截断)
服务端实现
配置 gRPC 传输器
// apps/main/src/main.ts
import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.GRPC,
options: {
package: 'hero', // 与 proto 文件中的 package 对应
protoPath: join(__dirname, 'hero/hero.proto'), // proto 文件路径
url: 'localhost:5000', // gRPC 服务监听地址
},
},
);
await app.listen();
}
bootstrap();
typescript
实现 gRPC 服务控制器
// apps/main/src/hero/hero.controller.ts
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
// 定义 TypeScript 接口(与 proto 消息对应)
interface HeroById {
id: number;
}
interface Hero {
id: number;
name: string;
}
interface HeroesService {
findOne(data: HeroById): Promise<Hero>;
}
@Controller()
export class HeroController implements HeroesService {
private readonly heroes = [
{ id: 1, name: '钢铁侠' },
{ id: 2, name: '蜘蛛侠' },
{ id: 3, name: '美国队长' },
];
@GrpcMethod('HeroesService', 'FindOne')
async findOne(data: HeroById): Promise<Hero> {
return this.heroes.find(hero => hero.id === data.id) || null;
}
}
typescript
客户端实现
方式一:ClientsModule 注册(推荐)
// apps/client/src/app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path';
import { AppService } from './app.service';
import { AppController } from './app.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE', // 注入令牌
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, '../../main/src/hero/hero.proto'),
url: 'localhost:5000', // gRPC 服务端地址
},
},
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
typescript
// apps/client/src/app.service.ts
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
interface HeroesService {
findOne(data: { id: number }): Observable<Hero>;
}
@Injectable()
export class AppService implements OnModuleInit {
private heroesService: HeroesService;
constructor(
@Inject('HERO_PACKAGE') private client: ClientGrpc,
) {}
onModuleInit() {
// 获取服务代理实例
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
async getHero(id: number) {
return this.heroesService.findOne({ id }).toPromise();
}
}
typescript
方式二:@Client() 装饰器(不推荐,但了解即可)
import { Injectable } from '@nestjs/common';
import { Client, Transport, ClientGrpc } from '@nestjs/microservices';
@Injectable()
export class AppService implements OnModuleInit {
@Client({
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero/hero.proto'),
},
})
private client: ClientGrpc;
private heroesService: HeroesService;
onModuleInit() {
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
}
typescript
Proto 转 TypeScript 的两种方案
方案一:使用 @grpc/proto-loader(动态加载)
运行时动态加载 .proto 文件并解析,NestJS 默认使用这种方式。
- 优点:无需额外构建步骤,开发方便
- 缺点:没有编译时的类型检查
方案二:使用 protoc + grpc-tools(静态生成)
使用 protoc 编译器将 .proto 文件编译为 TypeScript 代码:
# 安装工具
pnpm add -D grpc-tools grpc_tools_node_protoc_ts
# 生成 TypeScript 代码
./node_modules/.bin/grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./src/generated \
--ts_out=grpc_js:./src/generated \
-I ./protos \
./protos/hero.proto
bash
- 优点:编译时类型安全,IDE 提示完整
- 缺点:需要额外的构建步骤,proto 文件变更后需要重新生成
选择建议
| 场景 | 推荐方案 |
|---|---|
| 学习和快速原型 | 动态加载(proto-loader) |
| 生产项目 | 静态生成(grpc-tools) |
| 多语言协作团队 | 静态生成,确保类型一致 |
gRPC 与其他通信方式的对比
| 维度 | gRPC | REST (HTTP/JSON) | TCP | Redis Pub/Sub |
|---|---|---|---|---|
| 性能 | 极高 | 一般 | 高 | 高 |
| 类型安全 | 强类型 | 弱类型 | 无 | 无 |
| 跨语言 | 原生支持 | 需要 OpenAPI | 需自定义 | 需自定义 |
| 学习成本 | 需学 proto | 低 | 中 | 低 |
| 浏览器支持 | 需要 gRPC-Web | 原生支持 | 不支持 | 不支持 |
| 流式支持 | 双向流 | 需 WebSocket | 需自定义 | 仅发布/订阅 |
↑